﻿using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using Fluent.Logging;
using System.Drawing;
using System.Drawing.Imaging;

namespace Fluent.HtmlModel
{
    public class HtmlElement<TPage>
        where TPage : WebPageBase
    {
        public Finder Finder { get; private set; }
        private IWebElement _element;
        public IWebElement DriverElement
        {
            get
            {
                if (this._element == null)
                {
                    this._element = this.Finder.Find(this.Page);
                }

                return this._element;
            }
        }

        public TPage Page { get; protected set; }

        public HtmlElement(TPage page, Finder finder)
        {
            this.Page = page;
            this.Finder = finder;
        }

        public string this[string attributeName]
        {
            get
            {
                return this.DriverElement.GetAttribute(attributeName);
            }
        }

        #region Properties

        public string TagName
        {
            get
            {
                return this.DriverElement.TagName;
            }
        }

        public string OutHTML
        {
            get
            {
                return this["outHTML"];
            }
        }

        public string InnerHTML
        {
            get
            {
                return this["innerHTML"];
            }
        }

        public string InnerText
        {
            get
            {
                return this.DriverElement.Text;
            }
        }

        public string ID
        {
            get
            {
                return this["id"];
            }
        }

        public string Name
        {
            get
            {
                return this["name"];
            }
        }

        public string ClassName
        {
            get
            {
                return this["class"];
            }
        }

        public string Value
        {
            get
            {
                return this["value"];
            }
        }

        public int X
        {
            get
            {
                return this.DriverElement.Location.X;
            }
        }

        public int Y
        {
            get
            {
                return this.DriverElement.Location.Y;
            }
        }

        public string Width
        {
            get
            {
                return this.GetCssValue("width");
            }
        }

        public string Height
        {
            get
            {
                return this.GetCssValue("height");
            }
        }

        public string GetCssValue(string propertyName)
        {
            return this.DriverElement.GetCssValue(propertyName);
        }

        #endregion

        #region Elment Action API

        public virtual TPage CenterOnView()
        {
            var clientWidthObj = this.Page.Browser.ExecuteJS<int>("return document.documentElement.clientWidth;");
            var clientHeightObj = this.Page.Browser.ExecuteJS<int>("return document.documentElement.clientHeight;");

            if (clientHeightObj == null || clientWidthObj == null)
            {
                Log.Warning("Get current browser's client width: {0}, height: {1}",
                    clientHeightObj != null ? clientHeightObj.ToString() : "null",
                    clientWidthObj != null ? clientWidthObj.ToString() : "null");

                this.Page.Browser.Scroll(this.X, this.Y);
            }
            else
            {
                int clientWidth = Convert.ToInt32(clientWidthObj);
                int clientHeight = Convert.ToInt32(clientHeightObj);

                this.Page.Browser.Scroll(Math.Max(0, this.X - clientWidth / 2), Math.Max(0, this.Y - clientHeight / 2));
            }

            return this.Page;
        }

        public event Action<HtmlElement<TPage>> ClickFunc;
        public event Action<HtmlElement<TPage>> OnClicking;
        public event Action<HtmlElement<TPage>> OnClick;

        public virtual TPage Click()
        {
            this.WaitLoad();

            if (ClickFunc == null)
            {
                if (OnClicking != null) OnClicking(this);

                if (!this.DisableExpected) this.WaitShow();
                if (!this.NonVisibleExpected) this.CenterOnView();

                this.DriverElement.Click();

                if (OnClick != null) OnClick(this);
            }
            else
            {
                this.ClickFunc(this);
            }

            return this.Page;
        }

        public event Action<HtmlElement<TPage>> DoubleClickFunc;

        public virtual TPage DoubleClick()
        {
            this.WaitLoad();

            if (DoubleClickFunc == null)
            {
                new Actions(this.Page.Browser.GetDriver())
                    .DoubleClick(this.DriverElement)
                    .Perform();
            }
            else
            {
                DoubleClickFunc(this);
            }

            return this.Page;
        }

        public virtual TPage Focus()
        {
            throw new NotImplementedException();
        }

        public event Action<HtmlElement<TPage>> HoverFunc;

        public virtual TPage Hover()
        {
            this.WaitLoad();

            if (HoverFunc == null)
            {
                new Actions(this.Page.Browser.GetDriver())
                    .MoveToElement(this.DriverElement)
                    .Perform();
            }
            else
            {
                HoverFunc(this);
            }

            return this.Page;
        }

        #endregion

        #region Element States API

        public bool IsExist(double timeOut = 20, double sleepInterval = default(double))
        {
            return this.Page.Browser.Wait((driver) =>
            {
                return this.Finder.Finds(this.Page).Count > 0;
            },
                timeOut,
                sleepInterval);
        }

        /// <param name="timeOut">Set milliseconds timeout value for verify whether the HtmlControl enabled. Default value is set to 20 milliseconds.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public virtual bool IsEnabled(double timeOut = 20, double sleepInterval = default(double))
        {
            return this.Page.Browser.Wait((driver) =>
            {
                return this.DriverElement.Enabled;
            },
                timeOut,
                sleepInterval);
        }

        /// <param name="timeOut">Set milliseconds timeout value for verify whether the HtmlControl is visible. Default value is set to 20 milliseconds.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public virtual bool IsVisible(double timeOut = 20, double sleepInterval = default(double))
        {
            return this.Page.Browser.Wait((driver) =>
            {
                return this.DriverElement.Displayed;
            },
                timeOut,
                sleepInterval);
        }

        #endregion

        #region Element Wait API

        /// <summary>
        /// Wait the HtmlControl to loaded completely during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl loaded. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitLoad(double timeOut = default(double), double sleepInterval = default(double))
        {
            Log.Information("Start wait for element to load……");
            bool result = this.Page.Browser.Wait((driver) =>
            {
                return this.Finder.Finds(this.Page).Count > 0;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element to load failed! The element info: " + this.ToString());

            if (!result)
            {
                throw new Exception("Wait Load failed in " + timeOut.ToString() + " milliseconds");
            }

            Log.Information("Wait load done!");

            return this.Page;
        }

        /// <summary>
        /// Wait the HtmlControl to unloaded completely during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl unloaded. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitUnload(double timeOut = default(double), double sleepInterval = default(double))
        {
            this.Page.Browser.Wait((driver) =>
            {
                return this.Finder.Finds(this.Page).Count == 0;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element to unload failed! The element info: " + this.ToString());

            return this.Page;
        }

        /// <summary>
        /// Wait the HtmlControl to show up in UI during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl displayed. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitShow(double timeOut = default(double), double sleepInterval = default(double))
        {
            Log.Information("Start wait for element to show……");
            this.Page.Browser.Wait((driver) =>
            {
                return this.DriverElement.Displayed;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element to show failed! The element info: " + this.ToString());
            Log.Information("Wait show done!");

            return this.Page;
        }

        /// <summary>
        /// Wait the HtmlControl to hide in UI during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl hidden. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitHide(double timeOut = default(double), double sleepInterval = default(double))
        {
            this.Page.Browser.Wait((driver) =>
            {
                return !this.DriverElement.Displayed;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element to hide failed! The element info: " + this.ToString());

            return this.Page;
        }

        /// <summary>
        /// Wait the HtmlControl to enabled during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl to enabled. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitEnable(double timeOut = default(double), double sleepInterval = default(double))
        {
            this.Page.Browser.Wait((driver) =>
            {
                return this.DriverElement.Enabled;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element enable failed! The element info: " + this.ToString());

            return this.Page;
        }

        /// <summary>
        /// Wait the HtmlControl to disabled during a timeout setting.
        /// </summary>
        /// <param name="timeOut">Set milliseconds timeout value for wait the HtmlControl to disabled. Default value is set by App.config, TestSettings section group, pageTimeOut property value.</param>
        /// <param name="sleepInterval">Set milliseconds timeout value indicating how often to check for the condition to be true. Default is 500 milliseconds.</param>
        public TPage WaitDisable(double timeOut = default(double), double sleepInterval = default(double))
        {
            this.Page.Browser.Wait((driver) =>
            {
                return !this.DriverElement.Enabled;
            },
            timeOut,
            sleepInterval,
            errorMessage: "Wait element disable failed! The element info: " + this.ToString());

            return this.Page;
        }

        #endregion

        public TPage Snapshot(string fileName)
        {
            var clientWidthObj = this.Page.Browser.ExecuteJS<int>("return document.documentElement.clientWidth;");
            var clientHeightObj = this.Page.Browser.ExecuteJS<int>("return document.documentElement.clientHeight;");
            int clientWidth = Convert.ToInt32(clientWidthObj);
            int clientHeight = Convert.ToInt32(clientHeightObj);

            // Y+100: 100 is Browser Navigator Window panel height.
            int x = this.X, y = this.Y + 100, width = int.Parse(this.Width.Replace("px", "")), height = int.Parse(this.Height.Replace("px", ""));
            Log.Information("X: {0}; Y: {1}; Width: {2}; Height: {3};",
                x.ToString(),
                y.ToString(),
                width.ToString(),
                height.ToString());
            Bitmap bit = new Bitmap(width + 80, height + 80);

            using (var grah = Graphics.FromImage(bit))
            {
                grah.CopyFromScreen(x - 50, y - 50, 0, 0, bit.Size);
                bit.Save(fileName);
            }

            return this.Page;
        }

        public Bitmap Render(int x, int y, int width, int height, int? offsetX = null, int? offsetY = null, int? offsetW = null, int? offsetH = null)
        {
            Bitmap bit = new Bitmap(width, height);

            using (var grah = Graphics.FromImage(bit))
            {
                grah.CopyFromScreen(x, y, x + width, y + height, bit.Size);
            }

            return bit;
        }

        public override string ToString()
        {
            return string.Format("page: {0}"
                , Page.GetType().Name);
        }

        public virtual HtmlElement<TPage> ChangeFinder(Finder newFinder)
        {
            this.Finder = newFinder;
            this._element = null;
            return this;
        }

        public bool DisableExpected
        {
            get
            {
                return false;
                //throw new NotImplementedException();
            }
        }

        public bool NonVisibleExpected
        {
            get
            {
                return false;
                //throw new NotImplementedException();
            }
        }
    }
}